package com.bit.java_doc_searcher.searcher;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.ansj.domain.Term;
import org.ansj.splitWord.analysis.ToAnalysis;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

// 通过这个类在内存中来构造出索引结构

/**
 * 提供索引操作：
 * 1.getDocInfo：根据文档id，在正排索引中查询文档信息
 * 2.getInverted：根据搜索关键词，在倒排索引中查询文档列表
 * 3.addDoc：根据文档信息，构建索引（正排及倒排索引）
 * 4.save：构建索引阶段，把内存中的索引结构保存到磁盘中.
 * 5.load：项目启动阶段，把磁盘中的索引数据加载到内存中.
 */
public class Index {
    // 索引文件保存的目录路径
    private static String INDEX_PATH = null;

    static {
        // 初始化索引文件路径：区分云服务器和本地环境
        if (Config.isOnline) {
            // 云服务器中的索引目录路径
            INDEX_PATH = "/home/tz/install/doc_searcher_index/";
        } else {
            // 本地的索引目录路径
            INDEX_PATH = "E:\\TMP\\";
        }
    }

    // json处理框架jackson中的辅助类，用于json序列化及反序列化操作
    private ObjectMapper objectMapper = new ObjectMapper();

    // 正排索引：因为会根据文档id从正排索引中搜索文档，所以使用数组下标表示 docId，可以快速获取文档
    private ArrayList<DocInfo> forwardIndex = new ArrayList<>();

    // 倒排索引：使用 哈希表 来保存
    // key 就是搜索关键词，也就是搜索内容，分词以后的每个词
    // value 就是一组和这个词关联的文章列表（包含文档id和权重）
    private HashMap<String, ArrayList<Weight>> invertedIndex = new HashMap<>();

    // 多线程中构建正排索引，需要保证线程安全，使用对象来加锁
    private Object locker1 = new Object();
    // 多线程中构建倒排索引，需要保证线程安全，使用对象来加锁
    private Object locker2 = new Object();

    /**
     * 根据文档id，在正排索引中查询文档信息
     * @param docId
     * @return
     */
    public DocInfo getDocInfo(int docId) {
        return forwardIndex.get(docId);
    }

    /**
     * 根据搜索关键词，在倒排索引中查询文档列表
     * @param term 搜索关键词，为搜索内容分词后的一个词
     * @return 和这个词关联的文章列表（包含文档id和权重）
     */
    public List<Weight> getInverted(String term) {
        return invertedIndex.get(term);
    }

    /**
     * 根据文档信息，构建索引（正排及倒排索引）
     * @param title     标题
     * @param url       url
     * @param content   正文内容
     */
    public void addDoc(String title, String url, String content) {
        // 新增文档操作, 需要同时给正排索引和倒排索引新增信息
        // 构建正排索引
        DocInfo docInfo = buildForward(title, url, content);
        // 构建倒排索引
        buildInverted(docInfo);
    }

    /**
     * 根据文档信息构建倒排索引：
     * 1.统计标题的分词及词频
     * 2.统计正文的分词及词频
     * 其中标题和正文的权重是不同的，需要根据每个分词的词频及权重，来计算总的权重
     * 最终倒排索引中保存所有分词的权重及文档列表
     * @param docInfo
     */
    private void buildInverted(DocInfo docInfo) {
        // 方法内部类，用于统计每个分词在标题和正文中的词频
        class WordCnt {
            // 表示这个词在标题中出现的次数
            public int titleCount;
            // 表示这个词在正文中出现的次数
            public int contentCount;
        }
        // 这个数据结构用来统计词频
        HashMap<String, WordCnt> wordCntHashMap = new HashMap<>();

        // 1. 针对文档标题进行分词.
        List<Term> terms = ToAnalysis.parse(docInfo.getTitle()).getTerms();
        // 2. 遍历分词结果, 统计每个词出现的次数.
        for (Term term : terms) {
            // 先判定一下 term 是否存在.
            String word = term.getName();
            WordCnt wordCnt = wordCntHashMap.get(word);
            if (wordCnt == null) {
                // 如果不存在, 就创建一个新的键值对, 插入进去. titleCount 设为 1
                WordCnt newWordCnt = new WordCnt();
                newWordCnt.titleCount = 1;
                newWordCnt.contentCount = 0;
                wordCntHashMap.put(word, newWordCnt);
            } else {
                // 如果存在, 就找到之前的值, 然后把对应的 titleCount + 1
                wordCnt.titleCount += 1;
            }
        }
        // 3. 针对正文页进行分词.
        terms = ToAnalysis.parse(docInfo.getContent()).getTerms();
        // 4. 遍历分词结果, 统计每个词出现的次数.
        for (Term term : terms) {
            String word = term.getName();
            // 查询是否已存在统计数据
            WordCnt wordCnt = wordCntHashMap.get(word);
            // 不存在，则初始化
            if (wordCnt == null) {
                WordCnt newWordCnt = new WordCnt();
                newWordCnt.titleCount = 0;
                newWordCnt.contentCount = 1;
                wordCntHashMap.put(word, newWordCnt);
            // 存在，则自增
            } else {
                wordCnt.contentCount += 1;
            }
        }
        // 5. 把上面的结果汇总到一个 HashMap 里面.
        //    最终文档的权重, 就设定成标题中出现的次数 * 10 + 正文中出现的次数.
        // 6. 遍历刚才这个 HashMap, 依次来更新倒排索引中的结构了.
        for (Map.Entry<String, WordCnt> entry : wordCntHashMap.entrySet()) {
            // 先根据这里的词, 去倒排索引中查一查~
            // 倒排拉链
            // 多线程中构建倒排索引，为保证线程安全，先加锁
            synchronized (locker2) {
                // 根据搜索关键词，在倒排索引中查询对应的文档列表
                List<Weight> invertedList = invertedIndex.get(entry.getKey());
                // 不存在，则初始化
                if (invertedList == null) {
                    // 如果为空, 就插入一个新的键值对
                    ArrayList<Weight> newInvertedList = new ArrayList<>();
                    // 把新的文档(当前 searcher.DocInfo), 构造成 searcher.Weight 对象, 插入进来.
                    Weight weight = new Weight();
                    weight.setDocId(docInfo.getDocId());
                    // 权重计算公式: 标题中出现的次数 * 10 + 正文中出现的次数
                    weight.setWeight(entry.getValue().titleCount * 10 + entry.getValue().contentCount);
                    newInvertedList.add(weight);
                    // 倒排索引中添加每个分词及关联的文档列表
                    invertedIndex.put(entry.getKey(), newInvertedList);
                // 已存在，则更新：添加当前文档及权重信息到文档列表
                } else {
                    // 如果非空, 就把当前这个文档, 构造出一个 searcher.Weight 对象, 插入到倒排拉链的后面
                    Weight weight = new Weight();
                    weight.setDocId(docInfo.getDocId());
                    // 权重计算公式: 标题中出现的次数 * 10 + 正文中出现的次数
                    weight.setWeight(entry.getValue().titleCount * 10 + entry.getValue().contentCount);
                    invertedList.add(weight);
                }
            }
        }
    }

    /**
     * 根据文档信息构建正排索引
     * @param title     标题
     * @param url       url
     * @param content   正文内容
     * @return
     */
    private DocInfo buildForward(String title, String url, String content) {
        // 根据标题，url和正文来构造文档对象
        DocInfo docInfo = new DocInfo();
        docInfo.setTitle(title);
        docInfo.setUrl(url);
        docInfo.setContent(content);
        // 多线程中构建正排索引，为保证线程安全，先加锁
        synchronized (locker1) {
            // 将文档对象保存到正排索引中，下标即文档id
            docInfo.setDocId(forwardIndex.size());
            forwardIndex.add(docInfo);
        }
        // 返回保存的文档对象
        return docInfo;
    }

    /**
     * 把内存中的索引结构保存到磁盘中：
     * 1.正排索引
     * 2.倒排索引
     * 保存的数据为：将索引数据结构及其中Java对象序列化后的json格式字符串
     */
    public void save() {
        // 使用两个文件, 分别保存正排和倒排
        // 任务开始时间
        long beg = System.currentTimeMillis();
        System.out.println("保存索引开始!");
        // 1. 先判定一下索引对应的目录是否存在, 不存在就创建.
        File indexPathFile = new File(INDEX_PATH);
        if (!indexPathFile.exists()) {
            indexPathFile.mkdirs();
        }
        // 正排索引保存在本地的文件
        File forwardIndexFile = new File(INDEX_PATH + "forward.txt");
        // 倒排索引保存在本地的文件
        File invertedIndexFile = new File(INDEX_PATH + "inverted.txt");
        try {
            // 保存正排索引及数据到本地文件
            objectMapper.writeValue(forwardIndexFile, forwardIndex);
            // 保存倒排索引及数据到本地文件
            objectMapper.writeValue(invertedIndexFile, invertedIndex);
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 任务结束时间
        long end = System.currentTimeMillis();
        System.out.println("保存索引完成! 消耗时间: " + (end - beg) + " ms");
    }

    /**
     * 把磁盘中的索引数据加载到内存中：
     * 1.正排索引
     * 2.倒排索引
     * 加载的方式是：将json字符串，转换为正排及倒排数据结构及其中的Java对象
     */
    public void load() {
        // 任务开始时间
        long beg = System.currentTimeMillis();
        System.out.println("加载索引开始!");
        // 1. 先设置一下加载索引的路径
        File forwardIndexFile = new File(INDEX_PATH + "forward.txt");
        File invertedIndexFile = new File(INDEX_PATH + "inverted.txt");
        try {
            // 构建内存中的正排索引
            forwardIndex = objectMapper.readValue(forwardIndexFile, new TypeReference<ArrayList<DocInfo>>() {});
            // 构建内存中的倒排索引
            invertedIndex = objectMapper.readValue(invertedIndexFile, new TypeReference<HashMap<String, ArrayList<Weight>>>() {});
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 任务结束时间
        long end = System.currentTimeMillis();
        System.out.println("加载索引结束! 消耗时间: " + (end - beg) + " ms");
    }

    /**
     * 单元测试：可以用于本地验证代码
     * @param args
     */
    public static void main(String[] args) {
        Index index = new Index();
        index.load();
        System.out.println("索引加载完成");
    }
}
